package com.edroid.droidhelper.pm;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.ProtocolException;
import java.net.URL;

import org.json.JSONObject;

import com.edroid.droidhelper.pm.bean.PluginLoadResults;
import com.edroid.droidhelper.pm.bean.PluginVerifyResults;
import com.edroid.droidhelper.pm.utils.FileUtils;
import com.edroid.droidhelper.pm.utils.Logger;




/**
 * 新浪后台插件加载器，支持插件更新，dex加载
 * 插件默认存储在 私有/plugins/名字.jar
 * 
 * @author yichou 2014-3-4
 * 
 * <p><h3>2014-3-22：</h3>
 * <ol>
 *  <li>后缀改为 .jar 不使用软链接，避免导致错误
 * 	<li>更新到的插件先保存为 .jar.new，下次加载先判断并校验 .jar.new 文件
 * </ol>
 * </p>
 * <p><h3>2014-9-25
 * 	<li>引入 Logger，增加 PLUGIN2SD 设置
 * </p>
 * <p><h3>2014-9-29
 * 	<li>请求插件附件参数
 * </p>
 * <p><h3>2014-11-16
 * 	<li>下载过程文件锁保护（多进程同时加载一插件情况处理）
 *  <li>改用 handleMessage 模式处理异步
 * </p>
 * <p><h3>2014-111-30
 * 	<li>插件安全校验
 * </p>
 *
 */
public class PluginLoader {
	static final String TAG = PluginLoader.class.getSimpleName();
	static final Logger log = Logger.create(TAG);
	
	/**
	 * 默认模式（异步加载）
	 * 
	 * 先联网更新，如果有新版，下载完新版再加载，否则加载本地
	 * 无网络的情况先直接加载（同步）
	 */
	public static final int LOADE_MODE_DEFAULT = 0;
	
	/**
	 * 本地优先模式（同步加载，异步更新）
	 * 
	 * 先加载本地，再去联网更新插件
	 */
	public static final int LOADE_MODE_LOCAL_FIRST = 1;
	
	/**
	 * 仅加载本地
	 * 
	 * 如果本地没有则导致加载失败
	 */
	public static final int LOADE_MODE_LOCAL_ONLY = 2;
	private static final String API_URL = "http://edroid.sinaapp.com/getPlugin?";
	
	private File pluginFile, tmpFile, newFile;
	private final String name, serverName, urlParams;
	private int curVer = 0, newVer = 0, serverNewVer = 0;
	private boolean loaded = false;
	private PluginLoadCallback cb;
	private boolean update = false;
	private boolean localExist = false;
	
	
	private PluginLoader(String name, PluginLoadCallback cb, String urlParams) {
		this.cb = (cb==null? new PluginLoadCallback() : cb);
		this.name = name;
		this.serverName = name;
		this.urlParams = urlParams;
		
		final String dir = System.getProperty("user.dir") + File.separator + "data" + File.separator + "bin";
		
		pluginFile = new File(dir, name+".jar");
		tmpFile = new File(dir, name+".jar.tmp");
		newFile = new File(dir, name+".jar.new");
	}
	
	private static final int MSG_DOWNLOAD_ERROR = 0x1002;
	private static final int MSG_QUERRY_SERVER_ERROR = 0x1003;
	
	
	private int verifyCur() {
		int v = -1;
		curVer = -1;
		
		//先检测上次的插件更新文件
		if(newFile.exists()) {
			if((v = verifyPlg(newFile)) > 0) {
				pluginFile.delete();
				if(newFile.renameTo(pluginFile)) { //考虑到改名失败
					log.d("loca new ver=" + v);
					curVer = v;
				}
			} else { //删除破坏文件
				newFile.delete();
			}
		}
		
		//再检测本地已存在的，本地存在的必须比 new 小啊
		if(curVer < 0 && pluginFile.exists()) {
			if((v = verifyPlg(pluginFile)) > 0) {
				log.d("local cur ver=" + v);
				curVer = v;
			} else {
				pluginFile.delete();
			}
		}
		
		log.d("set curVer=" + curVer);
		
		return curVer;
	}
	
	boolean netAvailable() {
		return true;
	}
	
	public PluginLoadResults load(int loadMode) {
		PluginLoadResults ret = null;
		
		if(verifyCur() > 0) {
			if(loadMode == LOADE_MODE_LOCAL_ONLY || loadMode == LOADE_MODE_LOCAL_FIRST || !netAvailable()) //本地或无网络
				ret = loadPlugin();
			localExist = true;
		}
		
		if(loadMode != LOADE_MODE_LOCAL_ONLY && netAvailable())
			updatePlg();
		
		return ret;
	}
	
	public void update() {
		this.update  = true;
		
		if(verifyCur() > 0) {
			cb.onExist(curVer, pluginFile.getAbsolutePath());
		}
		
		updatePlg();
	}
	
	private PluginLoadResults loadPlugin() {
		log.d("load dex ver=" + curVer);
		
		final String path = pluginFile.getAbsolutePath();
		PluginLoadResults ret = new PluginLoadResults();
		ret.path = path;
		ret.ver = curVer;
		
		try {
			ret.classLoader = ClassLoaderHelper.load(path);
			cb.onLoadSuccess(ret);
			loaded = true;
			return ret;
		} catch (Exception e) {
			cb.onFailure(0, "create classloader fail!", e);
		}

		return null;
	}
	
	private void updatePlg() {
		new Thread(new Runnable() {
			
			@Override
			public void run() {
				querryServer();
			}
		}).start();
	}
	
	private void onError(int code, String msg, Exception e) {
		cb.onFailure(code, msg, e);
	}
	
	/**
	 * 新版插件下载成功后的处理逻辑
	 */
	private void onDlFinish() {
		boolean notifyUpdate = false;
		int oldVer = curVer;
		
		newVer = verifyPlg(newFile);
		if(newVer > 0) {
			log.d("dl suc newVer=" + newVer);
			if(newVer != serverNewVer) { //下载的版本号跟服务器版本号对比，以下载为准
				log.e("dl ver=" + newVer + ", but server Ver=" + serverNewVer);
			}
		}
		
		if(newVer > curVer) {
			curVer = newVer;
			pluginFile.delete();
			newFile.renameTo(pluginFile);
			notifyUpdate = true;
		}

		if(!update && !loaded) {
			loadPlugin();
		}
		
		/**
		 * onUpdateVer 仅仅的作用设计为通知插件有更新
		 */
		if(notifyUpdate) {
			//此时回调路径填什么好呢？
			cb.onUpdateVer(oldVer, newVer, pluginFile.getAbsolutePath());
		}
	}
	
	private int verifyPlg(File file) {
		if(!file.exists())
			return -1;
		
		try {
			PluginVerifyResults ret = PluginVerifyer.verifyEx(file);
			if(ret != null) {
				return ret.ver;
			} else {
				log.e("plugin verify fail!");
			}
		} catch (Exception e) {
		}
		
		return -1;
	}
	
	
	public static HttpURLConnection getDownloadConnection(String urlString) {
		try {
			URL url = new URL(urlString);

			HttpURLConnection conn = (HttpURLConnection) url.openConnection();

			conn.setAllowUserInteraction(true);
			conn.setRequestProperty("User-Agent", "NetFox");
			conn.setReadTimeout(5 * 1000); // 设置超时时间
			conn.setConnectTimeout(5 * 1000);
			conn.setRequestMethod("GET");
//			conn.addRequestProperty("Range", "bytes=" + startPos + "-");

			return conn;
		} catch (MalformedURLException e) {
		} catch (ProtocolException e) {
		} catch (IOException e) {
		}

		return null;
	}
	
	/**
	 * 下载插件逻辑，先下载到 tmpFile 下完 改名，下载过程锁住 tmp文件
	 * 
	 * 下载的时候应该校验 .new .jar 本地存在的 md5 与在线配置的 MD5 避免下载同样文件无线下载！
	 * @param url
	 */
	private void download(String url) {
		HttpURLConnection connection = null;
		InputStream is = null;
		com.edroid.droidhelper.pm.utils.FileUtils.MyFileLock lock = null;
		
		try {
			connection = getDownloadConnection(url);
			if(connection.getContentLength() > -1) {
				//第一步：先锁住 tmp 文件
				String lckFile = tmpFile.getAbsolutePath() + ".lck";
				lock = FileUtils.tryFileLock(lckFile);
				
				//如果被锁住了进行轮询，直到成功获取锁说明其他进程已经把这个文件下载完了，是不是可以直接拿去用了？
				if(lock == null) {
					do {
						try {
							Thread.sleep(1000);
						} catch (Exception e) {
						}
						lock = FileUtils.tryFileLock(lckFile);
					} while (lock == null);
					
					//插件下载完后的处理 newFile -> pluginFile 我们不能确定他处于哪个状态，
					//所以 干脆再让他执行一遍逻辑
					//或者将文件锁的释放放在 处理完插件加载之后？
				}

				// 使用 BufferedInputStream 提高性能
				is = new BufferedInputStream(connection.getInputStream()); 
				if(tmpFile.exists())
					tmpFile.delete();

				if(FileUtils.SUCCESS == FileUtils.streamToFile(tmpFile, is, false)) {
					if(newFile.exists())
						newFile.delete();
					tmpFile.renameTo(newFile);
					
					onDlFinish();
				} else {
					log.e("dl error, stream to file fail!");

					onError(MSG_DOWNLOAD_ERROR, "stream to file fail!", null);
				}
			}
		} catch (final Exception e) {
			log.e("dl error, " + e.getMessage());
			
			onError(MSG_DOWNLOAD_ERROR, e.getMessage(), e);
		} finally {
			try {
				connection.disconnect();
			} catch (Exception e2) {
			}
			
			try {
				is.close();
			} catch (Exception e2) {
			}
			
			FileUtils.releaseFileLock(lock);
		}
	}
	
	public static String is2String(InputStream is) throws IOException {
		StringBuilder sb = new StringBuilder(512);
		InputStreamReader reader = new InputStreamReader(is);
		char[] buf = new char[128];
		int read = 0;
		while((read = reader.read(buf)) != -1) {
			sb.append(buf, 0, read);
		}
		
		return sb.toString();
	}
	
	private void querryServer() {
		HttpURLConnection connection = null;
		InputStream is = null;
		
		try {
			String url = API_URL + "name=" + serverName + "&ver=" + curVer;
			if(urlParams != null)
				url += "&" + urlParams;
			if(log.isDebug())
				url += "&debug=1";
			
			connection = (HttpURLConnection) new URL(url).openConnection();
			is = connection.getInputStream();
			
			String jsonString = is2String(is);
			JSONObject jsonObject = new JSONObject(jsonString);
			int code = jsonObject.getInt("code");
			if(code == 200) {
				log.i("server rsp=" + jsonString);
				
				int ver = jsonObject.getInt("ver");
				if(ver > curVer) {
					serverNewVer = ver;
					
					log.d("server has new plugin, ver=" + ver);
					long t = System.currentTimeMillis();
					download(jsonObject.getString("url"));
					log.d("dl spen time:" + (System.currentTimeMillis() - t));
				}
			} else if (code == 201) { //201：无新版
				log.i("update no new!");
				
				if(!update && !loaded) { //服务器没有高于本地版本，如果不是立即加载本地方式，则在此加载
					if(localExist) {
						loadPlugin();
					} else {
						onError(MSG_QUERRY_SERVER_ERROR, "Server no such plugin \"" + name + "\"", null);
					}
				}
			}
		} catch (final Exception e) {
			log.e("update error! " + e.getMessage());
			
			if(!update && !loaded) { //服务器没有高于本地版本，如果不是立即加载本地方式，则在此加载
				if(localExist) {
					loadPlugin();
				} else {
					onError(MSG_QUERRY_SERVER_ERROR, "Querry server error: " + e.getMessage(), e);
				}
			}
		}
	}

	public static PluginLoadResults load(String name, PluginLoadCallback cb) {
		return load(name, null, LOADE_MODE_DEFAULT, cb);
	}

	public static PluginLoadResults load(String name, int loadMode, PluginLoadCallback cb) {
		return load(name, null, loadMode, cb);
	}

	/**
	 * 加载插件
	 * 
	 * @param context
	 * @param name
	 * @param urlParams 请求插件时附加参数
	 * @param cb
	 * @param loadMode 如果本地存在是否立即加载本地版本，再去服务器检查更新
	 * 
	 */
	public static PluginLoadResults load(String name, String urlParams, int loadMode, PluginLoadCallback cb) {
		return new PluginLoader(name, cb, urlParams).load(loadMode);
	}

	/**
	 * 更新插件
	 * 
	 * @param context
	 * @param name 插件名
	 * @param cb
	 */
	public static void update(String name, PluginLoadCallback cb) {
		new PluginLoader(name, cb, null).update();
	}

	/**
	 * 更新插件
	 * 
	 * @param context
	 * @param name
	 * @param urlParams 请求插件时附加参数
	 * @param cb
	 */
	public static void update(String name, String urlParams, PluginLoadCallback cb) {
		new PluginLoader(name, cb, urlParams).update();
	}
	
	
	public static class PluginLoadCallback {
		/**
		 * 检测到插件存在
		 * 
		 * @param curVer
		 * @param plgPath
		 */
		public void onExist(int curVer, String plgPath){}
		
		/**
		 * 下载到新版插件的回调
		 * 
		 * @param oldVer 老版本，当前版本，-1表示当前版本无效或者不存在
		 * @param newVer 新版本号
		 * @param plgPath 插件文件路径
		 */
		public void onUpdateVer(int oldVer, int newVer, String plgPath){}

		/**
		 * 调用 load 回调
		 */
		public void onLoadSuccess(PluginLoadResults ret){}
		
		public static final int ERR_CODE_DOWNLOAD_FAIL = 1;
		public static final int ERR_CODE_REQUEST_FAIL = 2;
		public static final int ERR_CODE_LOAD_FAIL = 3;
		
		/**
		 * 操作失败
		 * 
		 * @param code 错误码
		 * @param msg 错误消息
		 * @param e 错误堆栈
		 */
		public void onFailure(int code, String msg, Exception e){}
	}
}
